package com.sinszm.es.dao;

import cn.hutool.core.bean.BeanUtil;
import com.sinszm.common.exception.ApiException;
import com.sinszm.common.util.CommonUtils;
import com.sinszm.es.parser.DomainUtil;
import com.sinszm.es.support.EsConstant;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.nutz.json.Json;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static com.sinszm.es.support.EsConstant.TIME_OUT;

/**
 * 数据操作
 *
 * @author sinszm
 */
@ConditionalOnExpression("${szm.boot.es.enable:false}")
@Component
public class EsDao {

    private final RestClientBuilder esBuilder;

    private final RestHighLevelClient esClient;

    @Autowired
    public EsDao(
            @Qualifier(EsConstant.BUILDER_BEAN) RestClientBuilder esBuilder,
            @Qualifier(EsConstant.CLIENT_BEAN) RestHighLevelClient esClient
    ) {
        this.esBuilder = esBuilder;
        this.esClient = esClient;
    }

    /**
     * 节点及副本配置
     * @return  根据节点情况默认配置
     */
    private Settings.Builder settings() {
        int number = esBuilder.build().getNodes().size();
        return Settings.builder()
                .put("index.number_of_shards", number)
                .put("index.number_of_replicas", number <= 1 ? 0 : number);
    }

    /**
     * 客户端
     *
     * @return  操作客户端
     */
    public RestHighLevelClient es() {
        return esClient;
    }

    /**
     * 判断索引是否存在
     * @param index     索引名称
     * @return          是否存在，true存在，false不存在
     */
    public boolean existsIndex(String index) {
        GetIndexRequest request = new GetIndexRequest(index);
        try {
            return this.esClient.indices().exists(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            return false;
        }
    }

    /**
     * 判断索引是否存在
     * @param tClass    索引类
     * @param <T>       泛型
     * @return          是否存在，true存在，false不存在
     */
    public <T> boolean existsIndex(Class<T> tClass) {
        return existsIndex(DomainUtil.indexName(tClass));
    }

    /**
     * 创建索引
     * @param tClass        索引类
     * @param <T>           泛型
     * @return              创建结果
     */
    public <T> CreateIndexResponse createIndex(Class<T> tClass) {
        if (existsIndex(tClass)) {
            throw new ApiException("索引结构已存在");
        }
        CreateIndexRequest request = new CreateIndexRequest(DomainUtil.indexName(tClass));
        request.settings(this.settings());
        request.setTimeout(new TimeValue(TIME_OUT));
        request.mapping(DomainUtil.fieldName(tClass));
        try {
            return esClient.indices().create(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException("创建索引异常.");
        }
    }

    /**
     * 删除索引
     * @param tClass            索引类
     * @param <T>               泛型
     * @return                  删除结果
     */
    public <T> AcknowledgedResponse deleteIndex(Class<T> tClass) {
        if (!existsIndex(tClass)) {
            throw new ApiException("索引结构不存在");
        }
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(
                DomainUtil.indexName(tClass)
        );
        deleteIndexRequest.indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN);
        try {
            return esClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException("删除索引异常.");
        }
    }

    /**
     * 插入索引数据
     * @param data              数据
     * @param create            是否创建索引结构
     * @param <T>               泛型
     * @return                  插入结果
     */
    public <T> IndexResponse insert(T data, boolean create) {
        String id = DomainUtil.idValue(data);
        if (create) {
            if (existsIndex(data.getClass())) {
                deleteIndex(data.getClass());
            }
            createIndex(data.getClass());
        } else {
            if (!existsIndex(data.getClass())) {
                throw new ApiException("索引结构不存在");
            }
        }
        IndexRequest indexRequest = new IndexRequest(
                DomainUtil.indexName(data.getClass())
        ).id(id).source(DomainUtil.fieldValue(data));
        try {
            return esClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException("插入索引数据异常.");
        }
    }

    /**
     * 插入索引数据
     * <p>
     *     不执行索引结构的初始化创建
     * </p>
     * @param data              数据
     * @param <T>               泛型
     * @return                  插入结果
     */
    public <T> IndexResponse insert(T data) {
        return insert(data, false);
    }

    /**
     * 插入索引数据
     * <p>
     *     如果索引结构不存在则创建，如果索引存在则先删除结构再创建
     * </p>
     * @param data              数据
     * @param <T>               泛型
     * @return                  插入结果
     */
    public <T> IndexResponse insertEs(T data) {
        return insert(data, true);
    }

    /**
     * 更新索引数据
     * @param data              数据
     * @param <T>               泛型
     * @return                  更新结果
     */
    public <T> UpdateResponse update(T data) {
        String id = DomainUtil.idValue(data);
        if (!existsIndex(data.getClass())) {
            throw new ApiException("索引结构不存在");
        }
        UpdateRequest updateRequest = new UpdateRequest(DomainUtil.indexName(data.getClass()), id);
        updateRequest.doc(DomainUtil.fieldValue(data));
        try {
            return esClient.update(updateRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException("更新索引数据异常.");
        }
    }

    /**
     * 删除索引数据
     * @param data              数据
     * @param <T>               泛型
     * @return                  删除结果
     */
    public <T> DeleteResponse delete(T data) {
        String id = DomainUtil.idValue(data);
        if (!existsIndex(data.getClass())) {
            throw new ApiException("索引结构不存在");
        }
        DeleteRequest deleteRequest = new DeleteRequest(DomainUtil.indexName(data.getClass()), id);
        try {
            return esClient.delete(deleteRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException("删除索引数据异常.");
        }
    }

    /**
     * 查询单条索引数据详情
     * @param id            索引ID
     * @param tClass        实体
     * @param <T>           泛型
     * @return              数据
     */
    public <T> T findOne(String id, Class<T> tClass) {
        if (CommonUtils.isEmpty(id)) {
            throw new ApiException("索引ID不能为空");
        }
        if (tClass == null) {
            throw new ApiException("索引结构不能为空");
        }
        GetRequest request = new GetRequest(DomainUtil.indexName(tClass), id);
        try {
            GetResponse response = esClient.get(request,RequestOptions.DEFAULT);
            if (response.isExists()) {
                return BeanUtil.toBeanIgnoreError(Json.fromJsonAsMap(Object.class, response.getSourceAsString()), tClass);
            }
        } catch (IOException e) {
            e.printStackTrace(System.err);
        }
        return null;
    }

    /**
     * 查询全部
     * @param tClass        实体类
     * @param <T>           泛型
     * @return              索引列表
     */
    public <T> List<T> findAll(Class<T> tClass) {
        return findAll(tClass, 1000).getData();
    }

    /**
     * 查询全部
     * @param tClass    实体类
     * @param size      游标每页返回数
     * @param <T>       泛型
     * @return          索引列表
     */
    public <T> Page<T> findAll(Class<T> tClass, int size) {
        return query(QueryBuilders.matchAllQuery(), size, tClass);
    }

    /**
     * 查询数据
     * @param queryBuilder  查询条件
     * @param tClass        实体类
     * @param <T>           泛型
     * @return              按条件查询的数据列表
     */
    public <T> List<T> query(QueryBuilder queryBuilder, Class<T> tClass) {
        return query(queryBuilder, 1000, tClass).getData();
    }

    /**
     * 按Scroll查询数据
     * @param queryBuilder  查询条件
     * @param size          每次scroll返回数据条数
     * @param tClass        实体类
     * @param <T>           泛型
     * @return              分页数据
     */
    public <T> Page<T> query(QueryBuilder queryBuilder, int size, Class<T> tClass) {
        if (tClass == null) {
            throw new ApiException("索引结构不能为空");
        }
        //参数准备
        List<T> list = new ArrayList<>();
        int scrollSize = size <= 0 || size >= 10000 ? 1000 : size;
        //分页与查询条件组装
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(scrollSize);
        sourceBuilder.query(
                Optional.ofNullable(queryBuilder).orElse(QueryBuilders.matchAllQuery())
        );
        sourceBuilder.timeout(TimeValue.timeValueMinutes(1L));
        //请求组装，开启游标并保持1分钟
        SearchRequest searchRequest = new SearchRequest(new String[]{DomainUtil.indexName(tClass)}, sourceBuilder);
        searchRequest.scroll(TimeValue.timeValueMinutes(1L));
        try {
            int page = 0 ;
            SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
            if (response.status() != RestStatus.OK) {
                return new Page<>(scrollSize, 0, 0, list);
            }
            //第一页数据的处理
            SearchHit[] hits = response.getHits().getHits();
            for (SearchHit hit : hits) {
                list.add(BeanUtil.toBeanIgnoreError(
                        Json.fromJsonAsMap(Object.class, hit.getSourceAsString()), tClass
                ));
            }
            page ++ ;
            //执行游标查询后面页码的数据
            String scrollId = response.getScrollId();
            while (!ObjectUtils.isEmpty(hits)) {
                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
                scrollRequest.scroll(TimeValue.timeValueMinutes(1L));
                response = esClient.scroll(scrollRequest, RequestOptions.DEFAULT);
                if (response.status() != RestStatus.OK) {
                    break;
                }
                //处理数据
                scrollId = response.getScrollId();
                hits = response.getHits().getHits();
                if (!ObjectUtils.isEmpty(hits)) {
                    for (SearchHit hit : hits) {
                        list.add(BeanUtil.toBeanIgnoreError(
                                Json.fromJsonAsMap(Object.class, hit.getSourceAsString()), tClass
                        ));
                    }
                    page ++ ;
                }
            }
            //游标清理
            ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
            clearScrollRequest.addScrollId(scrollId);
            esClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
            return new Page<>(scrollSize, page, list.size(), list);
        } catch (IOException e) {
            e.printStackTrace(System.err);
        }
        return new Page<>(scrollSize, 0, list.size(), list);
    }

}
